package main

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/fsnotify/fsnotify"
)

type Event struct {
	Path string
	Op   Op
}

type Op uint32

const (
	Create Op = 1 << iota
	Write
	Remove
	Rename
)

type Watch struct {
	watcher   *fsnotify.Watcher
	eventTime map[string]int64
	paths     map[string]bool
	Events    chan Event
	Errors    chan error
}

func NewWatcher() (*Watch, error) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		return nil, err
	}
	return &Watch{
		watcher:   watcher,
		eventTime: make(map[string]int64),
		paths:     make(map[string]bool),
		Events:    make(chan Event),
		Errors:    make(chan error),
	}, nil
}

func (w *Watch) Close() {
	w.watcher.Close()
	w.watcher = nil
	w.eventTime = nil
	w.paths = nil
	close(w.Events)
	close(w.Errors)
}

func (w *Watch) Add(path string) error {
	err := w.watcher.Add(path)
	if err != nil {
		return err
	}
	w.paths[path] = true
	return nil
}

func (w *Watch) Remove(path string) error {
	err := w.watcher.Remove(path)
	if err != nil {
		return err
	}
	delete(w.eventTime, path)
	delete(w.paths, path)
	return nil
}

func (w *Watch) RemoveAll() error {
	for path := range w.paths {
		if err := w.Remove(path); err != nil {
			return err
		}
	}
	w.eventTime = make(map[string]int64)
	return nil
}

func (w *Watch) WatchDir(dirPath string) error {
	go func() {
		skip := false
		for {
			select {
			case ev, ok := <-w.watcher.Events:
				if !ok {
					return
				}
				skip = false

				mt := getFileModTime(ev.Name)
				if t := w.eventTime[ev.Name]; mt == t {
					// log.Printf("Skipping: %s\n", event.String())
					skip = true
				}

				w.eventTime[ev.Name] = mt

				if !skip {
					if ev.Op&fsnotify.Create == fsnotify.Create {
						fi, err := os.Stat(ev.Name)
						if err == nil && fi.IsDir() {
							err = w.Add(ev.Name)
							if err != nil {
								w.Errors <- err
							}
						} else {
							w.Events <- Event{Path: ev.Name, Op: Create}
						}
					}
					if ev.Op&fsnotify.Remove == fsnotify.Remove {
						fi, err := os.Stat(ev.Name)
						if err == nil && fi.IsDir() {
							err = w.Remove(ev.Name)
							if err != nil {
								w.Errors <- err
							}
						} else {
							w.Events <- Event{Path: ev.Name, Op: Remove}
						}
					}
					if ev.Op&fsnotify.Rename == fsnotify.Rename {
						err := w.Remove(ev.Name)
						if err != nil {
							w.Errors <- err
						} else {
							w.Events <- Event{Path: ev.Name, Op: Rename}
						}
					}
					if ev.Op&fsnotify.Write == fsnotify.Write {
						w.Events <- Event{Path: ev.Name, Op: Write}
					}
				}

			case err, ok := <-w.watcher.Errors:
				if !ok {
					return
				}
				w.Errors <- err
			}
		}
	}()

	err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
		if info.IsDir() {
			path, err := filepath.Abs(path)
			if err != nil {
				return err
			}
			err = w.watcher.Add(path)
			if err != nil {
				return err
			}
		}
		return nil
	})
	return err
}

// GetFileModTime returns unix timestamp of `os.File.ModTime` for the given path.
func getFileModTime(path string) int64 {
	path = strings.Replace(path, "\\", "/", -1)
	f, err := os.Open(path)
	if err != nil {
		fmt.Errorf("Failed to open file on '%s': %s", path, err)
		return time.Now().Unix()
	}
	defer f.Close()

	fi, err := f.Stat()
	if err != nil {
		fmt.Errorf("Failed to get file stats: %s", err)
		return time.Now().Unix()
	}

	return fi.ModTime().Unix()
}
